﻿/* 
 * 
 * Copyright 2013 Bendsoft. All rights reserved.
 * http://www.bendsoft.com
 * 
 * This code is distributed under the New BSD License (BSD).
 * 
 * @package     Camelot SharePoint Integration Library
 * @codeplex    http://camelottoolkit.codeplex.com
 * @license     New BSD License (BSD)
 * 
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Data;
using System.IO;

namespace Camelot.SharePointIntegration.Client
{
    public static class CamelotXml
    {
        public static string CreateXml(ListData data, bool writeCData)
        {
            if (data == null || data.Rows == null)
                throw new ArgumentException("This method requires valid list data", "data");

            var xmlSettings = new XmlWriterSettings();
            xmlSettings.Encoding = System.Text.Encoding.UTF8;
            xmlSettings.Indent = true;

            using (var sw = new Utf8StringWriter())
            {
                using (var writer = XmlWriter.Create(sw, xmlSettings))
                {
                    // begin document
                    writer.WriteStartDocument();
                    writer.WriteStartElement("List");

                    // list information
                    if (data.Description != null)
                    {
                        writer.WriteStartElement("ID");
                        writer.WriteValue(data.Description.ID.ToString());
                        writer.WriteEndElement();

                        writer.WriteStartElement("Title");
                        if (writeCData)
                            writer.WriteCData(data.Description.Title);
                        else
                            writer.WriteValue(data.Description.Title);
                        writer.WriteEndElement();

                        writer.WriteStartElement("Description");
                        if (writeCData)
                            writer.WriteCData(data.Description.Description);
                        else
                            writer.WriteValue(data.Description.Description);
                        writer.WriteEndElement();

                        writer.WriteStartElement("Created");
                        writer.WriteValue(data.Description.Created.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"));
                        writer.WriteEndElement();

                        writer.WriteStartElement("Modified");
                        writer.WriteValue(data.Description.Modified.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"));
                        writer.WriteEndElement();

                        writer.WriteStartElement("LastDeleted");
                        writer.WriteValue(data.Description.LastDeleted.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"));
                        writer.WriteEndElement();

                        writer.WriteStartElement("ItemCount");
                        writer.WriteValue(data.Description.ItemCount);
                        writer.WriteEndElement();

                        writer.WriteStartElement("BaseType");
                        writer.WriteValue((int)data.Description.BaseType);
                        writer.WriteEndElement();
                    }

                    // list schema
                    writer.WriteStartElement("Schema");
                    if (data.Schema != null)
                    {
                        foreach (DataRow row in data.Schema.AsEnumerable().Where(m => (bool)m["VisibleInView"]))
                        {
                            writer.WriteStartElement("Field");
                            writer.WriteStartAttribute("Name");
                            writer.WriteValue(XmlConvert.EncodeLocalName(row["DecodedName"].ToString()));
                            writer.WriteEndAttribute();

                            foreach (DataColumn col in data.Schema.Columns)
                            {
                                if (col.ColumnName != "Name" && !Convert.IsDBNull(row[col]))
                                {
                                    writer.WriteStartElement(XmlConvert.EncodeLocalName(col.ColumnName));
                                    if (writeCData && row[col] is string)
                                    {
                                        writer.WriteCData((string)row[col]);
                                    }
                                    else
                                    {
                                        writer.WriteValue(row[col].ToString());
                                    }
                                    writer.WriteEndElement();
                                }
                            }

                            var rowColumn = data.Rows.Columns.Cast<DataColumn>().Where(m => m.ColumnName == row["DecodedName"].ToString()).FirstOrDefault();
                            if (rowColumn != null)
                            {
                                writer.WriteStartElement("DataType");
                                writer.WriteValue(rowColumn.DataType.Name);
                                writer.WriteEndElement();
                            }

                            writer.WriteEndElement();
                        }
                    }
                    else
                    {
                        foreach (DataColumn col in data.Rows.Columns)
                        {
                            writer.WriteStartElement("Field");

                            writer.WriteStartAttribute("Name");
                            writer.WriteValue(XmlConvert.EncodeLocalName(col.ColumnName));
                            writer.WriteEndAttribute();

                            writer.WriteStartElement("DataType");
                            writer.WriteValue(col.DataType.Name);
                            writer.WriteEndElement();

                            writer.WriteEndElement();
                        }
                    }
                    writer.WriteEndElement();


                    // list content
                    Int32 rowIndex = 0;
                    writer.WriteStartElement("Content");
                    foreach (DataRow row in data.Rows.Rows)
                    {
                        writer.WriteStartElement("Row");
                        writer.WriteStartAttribute("Index");
                        writer.WriteValue(rowIndex);
                        writer.WriteEndAttribute();

                        foreach (DataColumn col in data.Rows.Columns)
                        {
                            writer.WriteStartElement(XmlConvert.EncodeLocalName(col.ColumnName));
                            if (row[col] != null && !Convert.IsDBNull(row[col]))
                            {
                                if (row[col] is DateTime)
                                {
                                    writer.WriteValue(((DateTime)(row[col])).ToString("yyyy-MM-dd HH:mm:ss"));
                                }
                                else if (writeCData && row[col] is string)
                                {
                                    writer.WriteCData((string)row[col]);
                                }
                                else if (row[col] is Array && !(row[col] is Byte[]))
                                {
                                    if (((Array)row[col]).Length != 0)
                                    {
                                        writer.WriteStartElement("Values");
                                        foreach (var value in (Array)row[col])
                                        {
                                            writer.WriteStartElement("Value");
                                            writer.WriteValue(value);
                                            writer.WriteEndElement();
                                        }
                                        writer.WriteEndElement();
                                    }
                                }
                                else if (row[col] is Guid)
                                {
                                    writer.WriteValue(((Guid)row[col]).ToString());
                                }
                                else
                                {
                                    writer.WriteValue(row[col]);
                                }
                            }
                            writer.WriteEndElement();
                        }

                        // row attachments
                        if (data.Attachments != null)
                        {
                            DataRow[] attachments = row.GetChildRows("Attachments");
                            if (attachments != null && attachments.Length > 0)
                            {
                                Int32 attachmentIndex = 0;
                                writer.WriteStartElement("AttachmentRows");
                                foreach (DataRow attachmentRow in attachments)
                                {
                                    writer.WriteStartElement("Row");
                                    writer.WriteStartAttribute("Index");
                                    writer.WriteValue(attachmentIndex);
                                    writer.WriteEndAttribute();

                                    foreach (DataColumn attachmentCol in attachmentRow.Table.Columns)
                                    {
                                        writer.WriteStartElement(XmlConvert.EncodeLocalName(attachmentCol.ColumnName));
                                        writer.WriteValue(attachmentRow[attachmentCol]);
                                        writer.WriteEndElement();
                                    }

                                    writer.WriteEndElement();
                                    attachmentIndex += 1;
                                }
                                writer.WriteEndElement();
                            }
                        }

                        writer.WriteEndElement();
                        rowIndex += 1;
                    }
                    writer.WriteEndElement();

                    // end document
                    writer.WriteEndElement();
                    writer.WriteEndDocument();
                }


                return sw.ToString();
            }
        }

        public static string CreateXml(ListData data)
        {
            return CreateXml(data, false);
        }

        public static ListData ReadXml(string xml)
        {
            var xmlDocument = new XmlDocument();
            xmlDocument.LoadXml(xml);

            var ID = xmlDocument.SelectSingleNode("List/ID");
            var Title = xmlDocument.SelectSingleNode("List/Title");
            var Description = xmlDocument.SelectSingleNode("List/Description");
            var Created = xmlDocument.SelectSingleNode("List/Created");
            var Modified = xmlDocument.SelectSingleNode("List/Modified");
            var LastDeleted = xmlDocument.SelectSingleNode("List/LastDeleted");
            var ItemCount = xmlDocument.SelectSingleNode("List/ItemCount");
            var BaseType = xmlDocument.SelectSingleNode("List/BaseType");

            var data = new ListData()
            {
                Description = ID != null ? new ListDescription()
                {
                    ID = new Guid(ID.InnerText),
                    Title = Title != null ? Title.InnerText : null,
                    Description = Description != null ? Description.InnerText : null,
                    Created = Created != null ? DateTime.ParseExact(Created.InnerText, "yyyy-MM-ddTHH:mm:ssZ", null) : DateTime.MinValue,
                    Modified = Modified != null ? DateTime.ParseExact(Modified.InnerText, "yyyy-MM-ddTHH:mm:ssZ", null) : DateTime.MinValue,
                    LastDeleted = LastDeleted != null ? DateTime.ParseExact(LastDeleted.InnerText, "yyyy-MM-ddTHH:mm:ssZ", null) : DateTime.MinValue,
                    ItemCount = ItemCount != null ? int.Parse(ItemCount.InnerText) : -1,
                    BaseType = BaseType != null ? (ListType)(int.Parse(BaseType.InnerText)) : ListType.Unspecified
                } : null,
                Content = new DataSet("Content")
            };

            var schemaFields = xmlDocument.SelectNodes("List/Schema/Field");

            if (xmlDocument.SelectSingleNode("List/Schema/Field/ID") != null)
            {
                var dtSchema = new DataTable("Schema");

                DataColumn[] schemaColumns = { 
                                             new DataColumn("ID", typeof(Guid)), 
                                             new DataColumn("DecodedName", typeof(string)),
                                             new DataColumn("DisplayName", typeof(string)),
                                             new DataColumn("Type", typeof(string)),
                                             new DataColumn("Required", typeof(bool)),
                                             new DataColumn("ReadOnly", typeof(bool)),
                                             new DataColumn("PrimaryKey", typeof(bool)),
                                             new DataColumn("Percentage", typeof(bool)),
                                             new DataColumn("RichText", typeof(bool)),
                                             new DataColumn("VisibleInView", typeof(bool)),
                                             new DataColumn("AppendOnly", typeof(bool)),
                                             new DataColumn("FillInChoice", typeof(bool)),
                                             new DataColumn("HTMLEncode", typeof(bool)),
                                             new DataColumn("Mult", typeof(bool)),
                                             new DataColumn("Filterable", typeof(bool)),
                                             new DataColumn("Sortable", typeof(bool))
                                         };

                dtSchema.Columns.AddRange(schemaColumns);

                foreach (XmlNode field in schemaFields)
                {
                    dtSchema.Rows.Add(
                        new Guid(field.SelectSingleNode("ID").InnerText),
                        field.SelectSingleNode("DecodedName").InnerText,
                        field.SelectSingleNode("DisplayName").InnerText,
                        field.SelectSingleNode("Type").InnerText,
                        bool.Parse(field.SelectSingleNode("Required").InnerText),
                        bool.Parse(field.SelectSingleNode("ReadOnly").InnerText),
                        bool.Parse(field.SelectSingleNode("PrimaryKey").InnerText),
                        bool.Parse(field.SelectSingleNode("Percentage").InnerText),
                        bool.Parse(field.SelectSingleNode("RichText").InnerText),
                        bool.Parse(field.SelectSingleNode("VisibleInView").InnerText),
                        bool.Parse(field.SelectSingleNode("AppendOnly").InnerText),
                        bool.Parse(field.SelectSingleNode("FillInChoice").InnerText),
                        bool.Parse(field.SelectSingleNode("HTMLEncode").InnerText),
                        bool.Parse(field.SelectSingleNode("Mult").InnerText),
                        bool.Parse(field.SelectSingleNode("Filterable").InnerText),
                        bool.Parse(field.SelectSingleNode("Sortable").InnerText)
                        );
                }

                data.Content.Tables.Add(dtSchema);
            }

            var dtRows = new DataTable("Rows");

            foreach (XmlNode field in schemaFields)
            {
                if (field.SelectSingleNode("DataType") == null)
                    throw new Exception("Invalid or incomplete schema. Missing DataType attribute.");

                if (field.Attributes["Name"] == null)
                    throw new Exception("Invalid or incomplete schema. Missing Name attribute.");

                var typeName = field.SelectSingleNode("DataType").InnerText;
                var dataType = System.Type.GetType("System." + typeName, true);
                var columnName = XmlConvert.DecodeName(field.Attributes["Name"].Value);

                dtRows.Columns.Add(columnName, dataType);
            }

            var contentRows = xmlDocument.SelectNodes("List/Content/Row");

            foreach (XmlNode row in contentRows)
            {
                var dataRow = dtRows.NewRow();
                foreach (XmlNode column in row.ChildNodes)
                {
                    if ((column as XmlElement).IsEmpty)
                    {
                        dataRow[column.XmlDecodeName()] = DBNull.Value;
                    }
                    else
                    {
                        if (dtRows.Columns[column.XmlDecodeName()].DataType == typeof(Guid))
                        {
                            dataRow[column.XmlDecodeName()] = new Guid(column.InnerText);
                        }

                        if (dtRows.Columns[column.XmlDecodeName()].DataType == typeof(DateTime))
                        {
                            dataRow[column.XmlDecodeName()] = DateTime.ParseExact(column.InnerText, "yyyy-MM-dd HH:mm:ss", null);
                        }

                        if (dtRows.Columns[column.XmlDecodeName()].DataType == typeof(string))
                        {
                            dataRow[column.XmlDecodeName()] = column.InnerText;
                        }

                        if (dtRows.Columns[column.XmlDecodeName()].DataType == typeof(bool))
                        {
                            dataRow[column.XmlDecodeName()] = bool.Parse(column.InnerText);
                        }

                        if (dtRows.Columns[column.XmlDecodeName()].DataType == typeof(int))
                        {
                            dataRow[column.XmlDecodeName()] = int.Parse(column.InnerText);
                        }

                        if (dtRows.Columns[column.XmlDecodeName()].DataType == typeof(double))
                        {
                            dataRow[column.XmlDecodeName()] = double.Parse(column.InnerText, System.Globalization.CultureInfo.GetCultureInfo("en-US").NumberFormat);
                        }

                        if (dtRows.Columns[column.XmlDecodeName()].DataType == typeof(int[]))
                        {
                            var values = column.SelectNodes("Values/Value");
                            if (values != null)
                                dataRow[column.XmlDecodeName()] = values.OfType<XmlNode>().Select(m => int.Parse(m.InnerText)).ToArray();
                        }

                        if (dtRows.Columns[column.XmlDecodeName()].DataType == typeof(string[]))
                        {
                            var values = column.SelectNodes("Values/Value");
                            if (values != null)
                                dataRow[column.XmlDecodeName()] = values.OfType<XmlNode>().Select(m => m.InnerText).ToArray();
                        }

                        if (dtRows.Columns[column.XmlDecodeName()].DataType == typeof(byte[]))
                        {
                            dataRow[column.XmlDecodeName()] = Convert.FromBase64String(column.InnerText);
                        }

                        if (Convert.IsDBNull(dataRow[column.XmlDecodeName()]))
                            throw new Exception(string.Format("System type not supported: {0}", dataRow[column.XmlDecodeName()].GetType().Name));
                    }
                }
                dtRows.Rows.Add(dataRow);
            }

            data.Content.Tables.Add(dtRows);
            return data;
        }

        private class Utf8StringWriter : StringWriter
        {
            public override Encoding Encoding
            {
                get { return Encoding.UTF8; }
            }
        }
    }
}
